Scope parsing, classification & policy config#4176
Scope parsing, classification & policy config#4176stevenvegt wants to merge 5 commits intofeature/4144-mixed-scopesfrom
Conversation
|
Coverage Impact ⬆️ Merging this pull request will increase total coverage on Modified Files with Diff Coverage (4)
🤖 Increase coverage with AI coding...🚦 See full report on Qlty Cloud » 🛟 Help
|
Rename the PDPBackend interface method and introduce new types (CredentialProfileMatch, ScopePolicy, credentialProfileConfig) to support mixed OAuth2 scopes. The policy config struct now uses explicit fields for organization/user PDs and scope_policy, defaulting to profile-only. All callers and mocks updated. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Tests cover: multi-scope with one profile scope + other scopes, multiple profile scopes (error), no profile scope (error), and empty scope string (error). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Tests cover: scope_policy parsed from JSON config (dynamic, passthrough), invalid scope_policy rejected at load time, dynamic without AuthZen endpoint fails at startup, passthrough without endpoint succeeds. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Introduce ErrAmbiguousScope for multiple credential profile scopes (instead of wrapping ErrNotFound which was semantically wrong) - Use strings.Fields instead of strings.Split for robust whitespace handling - Add nil-check: credential profile must define at least one of organization/user - Add doc comments on Config, ErrNotFound, FindCredentialProfile implementation - Use value receiver on toWalletOwnerMapping (small non-mutating struct) - Add test for consecutive spaces in scope string - Assert ScopePolicy in multi-scope test - Make Configure tests load single files instead of whole directory Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
| // ScopePolicyProfileOnly only accepts the credential profile scope. Extra scopes cause an error. | ||
| ScopePolicyProfileOnly ScopePolicy = "profile-only" | ||
| // ScopePolicyPassthrough grants all requested scopes without evaluation. | ||
| ScopePolicyPassthrough ScopePolicy = "passthrough" |
There was a problem hiding this comment.
maybe we should remove this after the PoCs and "dynamic" is implemented? Because it sounds quite unsafe.
| // CredentialProfileMatch is the result of matching a scope string against the policy configuration. | ||
| // It contains the matched credential profile (WalletOwnerMapping + ScopePolicy) and the | ||
| // remaining scopes that did not match any credential profile. | ||
| type CredentialProfileMatch struct { |
There was a problem hiding this comment.
i still find this abstraction a bit weird
| // FindCredentialProfile resolves a scope string against the policy configuration. | ||
| // It parses the space-delimited scope string, identifies exactly one credential profile scope, | ||
| // and returns the matched profile along with any remaining scopes. | ||
| FindCredentialProfile(ctx context.Context, scope string) (*CredentialProfileMatch, error) |
There was a problem hiding this comment.
Shouldn't it be named "MatchCredentialProfile" if it returns a Match?
| // CredentialProfileScope is the scope that matched a credential profile. | ||
| CredentialProfileScope string |
There was a problem hiding this comment.
Odd naming, it repeats its container, but just "Scope" might be too ambiguous. Abstraction/naming is off, but I can't think of a better one right now

Parent PRD
#4144
Summary
Foundation layer for mixed OAuth2 scope support. Renames
PDPBackend.PresentationDefinitions()toFindCredentialProfile()and enriches its return type to include the credential profile scope, its policy, and any remaining ("other") scopes. Extends the policy JSON format with ascope_policyfield and adds apolicy.authzen.endpointCLI flag validated at startup.What changed
Policy interface (
policy/interface.go,policy/local.go):PresentationDefinitions→FindCredentialProfile, returning*CredentialProfileMatch(scope, WalletOwnerMapping, scope policy, other scopes) instead of justpe.WalletOwnerMapping.strings.Fields(tolerant of tabs, newlines, consecutive spaces).ErrAmbiguousScope(multiple profile scopes in one request), distinct fromErrNotFound.Policy JSON format (
policy/local.go,policy/test/scope_policy/):scope_policyfield per credential profile:"profile-only"(default),"passthrough", or"dynamic".scope_policycontinue to work (defaults toprofile-only).credentialProfileConfigreplaces the map-basedvalidatingWalletOwnerMappingwith an explicit struct (organization/user fields + scope_policy).validatingPresentationDefinition— moved from mapping-level to per-PD level.Config & validation (
policy/config.go,policy/cmd.go,policy/local.go):policy.authzen.endpointCLI flag.dynamicwithout an AuthZen endpoint configured.passthroughandprofile-onlycontinue to work without an endpoint.Callers updated (
auth/api/iam/):validation.goandapi.goupdated for the new return type, withErrAmbiguousScopemapped toinvalid_scope.*CredentialProfileMatch.How to review
Start with
policy/interface.go— understand the new types (CredentialProfileMatch,ScopePolicy) and the renamed interface method.Then
policy/local.go— the core logic:FindCredentialProfile(scope parsing + classification)credentialProfileConfig.UnmarshalJSON(JSON format handling)Configure(startup validation for AuthZen endpoint)Tests to focus on (
policy/local_test.go):TestLocalPDP_FindCredentialProfile— multi-scope parsing edge cases (consecutive whitespace, empty strings, multiple profile scopes, zero profile scopes)TestLocalPDP_ScopePolicyConfig— JSON parsing, invalid value rejectionTestLocalPDP_Configure— startup validation behaviorCallers (
auth/api/iam/) are mechanical updates — they extract.WalletOwnerMappingfrom the result to preserve existing behavior.Deviations from spec
credentialProfileConfiguses an explicit struct withOrganization/Userfields, not the map-based approach suggested in the spec. This avoided custom JSON parsing of the scope_policy field while preserving the existing flat JSON format. PD validation moved to per-PD level (still uses the v2 schema).ErrAmbiguousScopeas a distinct error (not in spec). WrappingErrNotFoundfor "too many profile scopes" was semantically wrong.organization/user— catches typos likeorganisationsthat standard struct unmarshaling would silently accept.strings.Fieldsinstead ofstrings.Split— handles tabs, newlines, and consecutive spaces robustly.Dependencies
None — this is the foundation PR. Subsequent PRs #4177–#4180 build on this interface.
Design context
passthroughmode was added in-scope after initial PR split (see PRD comment 4235711103)Acceptance Criteria
FindCredentialProfile()accepts space-delimited multi-scope strings (usesstrings.Fieldsfor robust whitespace handling)ErrNotFoundfor zero,ErrAmbiguousScopefor multiple)scope_policyfield parsed from policy config (defaults to"profile-only")scope_policyvalues rejected at load timeorganizationoruserpolicy.authzen.endpointconfig field and CLI flag addeddynamicscope policy is used without AuthZen endpointpassthroughandprofile-onlywork without AuthZen endpointvalidatingPresentationDefinition(validates against v2 JSON schema)Original implementation spec (used during AI-assisted development)
Parent PRD
#4144
Implementation Spec
Overview
Foundation layer for mixed OAuth2 scope support. This PR modifies the policy subsystem to:
scope_policyfield per credential profile.policy.authzen.endpointconfiguration field for the AuthZen PDP URL.scope_policy: "dynamic"requires an AuthZen endpoint.Key files modified
policy/interface.go— RenamedPDPBackend.PresentationDefinitions()toFindCredentialProfile(), addedCredentialProfileMatch,ScopePolicy,ErrAmbiguousScopetypespolicy/local.go— NewcredentialProfileConfigstruct with explicit Organization/User fields, multi-scope parsing viastrings.Fields, scope policy defaulting and validationpolicy/config.go— AddedAuthZenConfigwith endpoint fieldpolicy/cmd.go— Addedpolicy.authzen.endpointCLI flagauth/api/iam/validation.go— Updated caller to useFindCredentialProfile, handlesErrAmbiguousScopeauth/api/iam/api.go— Updated caller to useFindCredentialProfileDesign
Interface change
Renamed
PresentationDefinitionstoFindCredentialProfileto better reflect the method's responsibility: resolving a scope string against the policy configuration to find the matching credential profile.Policy configuration format
The per-scope JSON structure now supports
scope_policyalongside the existingorganization/userkeys:{ "urn:nuts:medication-overview": { "organization": { /* PD */ }, "scope_policy": "profile-only" } }Internally,
credentialProfileConfiguses a struct with explicit fields (instead of the old map-basedvalidatingWalletOwnerMapping), allowing standard JSON unmarshaling. Individual PDs are validated against the v2 JSON schema viavalidatingPresentationDefinition.Error handling
ErrNotFound— no credential profile scope matchedErrAmbiguousScope— multiple credential profile scopes found in the requestAcceptance Criteria
FindCredentialProfile()accepts space-delimited multi-scope strings (usesstrings.Fieldsfor robust whitespace handling)ErrNotFoundfor zero,ErrAmbiguousScopefor multiple)scope_policyfield parsed from policy config (defaults to"profile-only")scope_policyvalues rejected at load timeorganizationoruserpolicy.authzen.endpointconfig field and CLI flag addeddynamicscope policy is used without AuthZen endpointpassthroughandprofile-onlywork without AuthZen endpointvalidatingPresentationDefinition(validates against v2 JSON schema)